<!doctype html>
<html lang="zh-CN">
<head>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    
    <meta name="referrer" content="no-referrer-when-downgrade">
    

    <title>深入浅出之RenderObject | 早起的年轻人</title>
    <meta property="og:title" content="深入浅出之RenderObject - 早起的年轻人">
    <meta property="og:type" content="article">
        
    <meta property="article:published_time" content='2021-09-14T10:09:30&#43;08:00'>
        
        
    <meta property="article:modified_time" content='2021-09-14T10:09:30&#43;08:00'>
        
    <meta name="Keywords" content="Flutter，golang,go语言,go语言笔记,飞雪无情,java,android,博客,项目管理,python,软件架构,公众号,小程序">
    <meta name="description" content="深入浅出之RenderObject">
        
    <meta name="author" content="luckly">
    <meta property="og:url" content="https://luckly.work/post/flutter_Framework/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E4%B9%8BRenderObject/">
    <link rel="shortcut icon" href='/favicon.ico'  type="image/x-icon">

    <link rel="stylesheet" href='/css/normalize.css'>
    <link rel="stylesheet" href='/css/style.css'>
    <script type="text/javascript" src="//cdn.bootcdn.net/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

    
    
    
        <link href="https://cdn.bootcdn.net/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.css" rel="stylesheet">
    
    
    
    
        <link rel="stylesheet" href='/css/douban.css'>
    
        <link rel="stylesheet" href='/css/other.css'>
    
</head>

<link rel="stylesheet"
      href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/highlight.min.js"></script>
<body>
    <header id="header" class="clearfix">
    <div class="container">
        <div class="col-group">
            <div class="site-name ">
                
                    <a id="logo" href="https://luckly.work/">
                        早起的年轻人
                    </a>
                
                <p class="description">专注于Flutter、Android、Java、Go语言(golang)、移动互联网、项目管理、软件架构</p>
            </div>
            <div>
                <nav id="nav-menu" class="clearfix">
                    <a class="current" href="https://luckly.work/">首页</a>
                    
                    <a  href="https://luckly.work/categories/" title="分类">分类</a>
                    
                    <a  href="https://luckly.work/tags/" title="标签">标签</a>
                    
                    <a  href="https://luckly.work/archives/" title="归档">归档</a>
                    
                    <a  href="https://luckly.work/about/" title="关于我">关于我</a>
                    
                    <a  href="https://github.com/ITmxs/" title="github">github</a>
                    
                </nav>
            </div>
        </div>
    </div>
</header>

    <div id="body">
        <div class="container">
            <div class="col-group">

                <div class="col-8" id="main">
                    
<div class="res-cons">
    <style type="text/css">
    .post-toc {
        position: fixed;
        width: 200px;
        margin-left: -210px;
        padding: 5px 10px;
        font-family: Athelas, STHeiti, Microsoft Yahei, serif;
        font-size: 12px;
        border: 1px solid rgba(0, 0, 0, .07);
        border-radius: 5px;
        background-color: rgba(255, 255, 255, 0.98);
        background-clip: padding-box;
        -webkit-box-shadow: 1px 1px 2px rgba(0, 0, 0, .125);
        box-shadow: 1px 1px 2px rgba(0, 0, 0, .125);
        word-wrap: break-word;
        white-space: nowrap;
        -webkit-box-sizing: border-box;
        box-sizing: border-box;
        z-index: 999;
        cursor: pointer;
        max-height: 70%;
        overflow-y: auto;
        overflow-x: hidden;
    }

    .post-toc .post-toc-title {
        width: 100%;
        margin: 0 auto;
        font-size: 20px;
        font-weight: 400;
        text-transform: uppercase;
        text-align: center;
    }

    .post-toc .post-toc-content {
        font-size: 15px;
    }

    .post-toc .post-toc-content>nav>ul {
        margin: 10px 0;
    }

    .post-toc .post-toc-content ul {
        padding-left: 20px;
        list-style: square;
        margin: 0.5em;
        line-height: 1.8em;
    }

    .post-toc .post-toc-content ul ul {
        padding-left: 15px;
        display: none;
    }

    @media print,
    screen and (max-width:1057px) {
        .post-toc {
            display: none;
        }
    }
</style>
<div class="post-toc" style="position: absolute; top: 188px;">
    <h2 class="post-toc-title">文章目录</h2>
    <div class="post-toc-content">
        <nav id="TableOfContents">
  <ul>
    <li><a href="#markneedslayout调用场景">markNeedsLayout调用场景</a></li>
    <li><a href="#relayout-boundary">Relayout Boundary</a></li>
    <li><a href="#markneedslayout">markNeedsLayout</a></li>
    <li><a href="#layout">layout</a></li>
    <li><a href="#performlayout">performLayout</a></li>
  </ul>

  <ul>
    <li><a href="#markneedspaint">markNeedsPaint</a></li>
    <li><a href="#repaint-boundary">Repaint Boundary</a></li>
    <li><a href="#paint">Paint</a></li>
    <li><a href="#串起来">串起来</a>
      <ul>
        <li><a href="#pipelineownerflushpaint">PipelineOwner#flushPaint</a></li>
        <li><a href="#paintingcontextrepaintcompositedchild">PaintingContext#repaintCompositedChild</a></li>
        <li><a href="#renderobject_paintwithcontext">RenderObject#_paintWithContext</a></li>
        <li><a href="#paintingcontextpaintchild">PaintingContext#paintChild</a></li>
        <li><a href="#paintingcontext_compositechild">PaintingContext#_compositeChild</a></li>
        <li><a href="#小结">小结</a></li>
      </ul>
    </li>
  </ul>
</nav>
    </div>
</div>
<script type="text/javascript">
    $(document).ready(function () {
        var postToc = $(".post-toc");
        if (postToc.length) {
            var leftPos = $("#main").offset().left;
            if(leftPos<220){
                postToc.css({"width":leftPos-10,"margin-left":(0-leftPos)})
            }

            var t = postToc.offset().top - 20,
                a = {
                    start: {
                        position: "absolute",
                        top: t
                    },
                    process: {
                        position: "fixed",
                        top: 20
                    },
                };
            $(window).scroll(function () {
                var e = $(window).scrollTop();
                e < t ? postToc.css(a.start) : postToc.css(a.process)
            })
        }
    })
</script>
    <article class="post">
        <header>
            <h1 class="post-title">深入浅出之RenderObject</h1>
        </header>
        <date class="post-meta meta-date">
            2021年9月14日
        </date>
        
        <div class="post-meta">
            <span>|</span>
            
            <span class="meta-category"><a href='/categories/Flutter'>Flutter</a></span>
            
        </div>
        
        
        <div class="post-meta">
            <span id="busuanzi_container_page_pv">|<span id="busuanzi_value_page_pv"></span><span>
                    阅读</span></span>
        </div>
        
        
        <div class="post-content">
            <p>本文是『 深入浅出 Flutter Framework 』系列文章的第七篇。</p>
<h1 id="overview">Overview</h1>
<hr>
<p>可以说，RenderObject 在整个 Flutter Framework 中属于核心对象，其职责概括起来主要有三点：
『 Layout 』、『 Paint 』、『 Hit Testing 』。
然而，RenderObject 是抽象类，具体工作由子类去完成。
<a href="https://zxfcumtcs.github.io/img/RenderObjectClassDiagram.png">
        <a data-fancybox="gallery" href="https://zxfcumtcs.github.io/img/RenderObjectClassDiagram.png">
            <img class="mx-auto" alt="img" src="https://zxfcumtcs.github.io/img/RenderObjectClassDiagram.png" />
        </a>
    </a>
如上图，<code>RenderSliver</code>、<code>RenderBox</code>、<code>RenderView</code>以及<code>RenderAbstractViewport</code>是在 Flutter Framework 中定义的4个子类：</p>
<ul>
<li><code>RenderSliver</code>，『 Sliver-Widget 』对应的 Base RenderObject；</li>
<li><code>RenderBox</code>，除『 Sliver-Widget 』外几乎所有常见 Render-Widget 对应的 Base RenderObject；</li>
<li><code>RenderView</code>，是一种特殊的 Render Object，是『 RenderObect Tree 』的根节点；</li>
<li><code>RenderAbstractViewport</code>，主要用于『 Scroll-Widget 』。</li>
</ul>
<p><a href="https://zxfcumtcs.github.io/img/RenderObject_XMind.png">
        <a data-fancybox="gallery" href="https://luckly007.oss-cn-beijing.aliyuncs.com/image/RenderObject_XMind.png">
            <img class="mx-auto" alt="img" src="https://luckly007.oss-cn-beijing.aliyuncs.com/image/RenderObject_XMind.png" />
        </a>
    </a>
如上图，概括了<code>RenderObject</code>中与 Layout、Paint 相关的主要属性与方法(也是本文将要讨论的主要内容)。
其中，用虚线框起来的是<code>RenderObject</code>子类需要重写的方法。
正如上面所说，<code>RenderObject</code>是抽象类，它即没有明确使用哪种坐标系 (Cartesian coordinates or Polar coordinates)，也没有指定使用哪种排版算法 (width-in-height-out or constraint-in-size-out)。</p>
<blockquote>
<p><code>RenderBox</code>采用笛卡尔坐标系、排版算法是 constraint-in-size-out，即根据父节点传递的排版约束来计算 Size。</p>
</blockquote>
<p>下面我们从<code>RenderObject</code>生命周期中几个关键节点展开介绍：创建、布局、渲染。</p>
<blockquote>
<p>本文所示代码基于 Flutter 1.12.13，同时对代码做了精简处理，以便突出所要讨论的重点。</p>
</blockquote>
<h1 id="创建">创建</h1>
<hr>
<p>在<a href="https://zxfcumtcs.github.io/2020/05/17/deepinto-flutter-element/">『深入浅出 Flutter Framework 之 Element』</a>一文中我们介绍过，当 RenderObjectElement 被挂载(mount) 到『 Element Tree 』上时，会创建对应的 Render Object 。
同时，会将其 attach 到『 RenderObject Tree 』上，也就是在『 Element Tree 』创建过程中『 RenderObject Tree 』也被逐步创建出来：</p>
<pre><code>// RenderObjectElement
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}
</code></pre><h1 id="布局">布局</h1>
<hr>
<p>在<a href="https://zxfcumtcs.github.io/2020/12/05/deepinto-flutter-pipelineowner/">『深入浅出 Flutter Framework 之 PipelineOwner』</a>一文中介绍过，当 RenderObject 需要(重新)布局时调用<code>markNeedsLayout</code>方法，从而被<code>PipelineOwner</code>收集，并在下一帧刷新时触发 Layout 操作。</p>
<h2 id="markneedslayout调用场景">markNeedsLayout调用场景</h2>
<ul>
<li>
<p>Render Object 被添加到『 RenderObject Tree 』;</p>
</li>
<li>
<p>子节点 adopt、drop、move；</p>
</li>
<li>
<p>由子节点的<code>markNeedsLayout</code>方法传递调用；</p>
</li>
<li>
<p>Render Object 自身与布局相关的属性发生变化，如</p>
<pre><code>RenderFlex
</code></pre><p>的排版方向有变化时：</p>
<pre><code>set direction(Axis value) {
  assert(value != null);
  if (_direction != value) {
    _direction = value;
    markNeedsLayout();
  }
}
</code></pre></li>
</ul>
<h2 id="relayout-boundary">Relayout Boundary</h2>
<p>若某个 Render Object 的布局变化不会影响到其父节点的布局，则该 Render Object 就是『 Relayout Boundary 』。
Relayout Boundary 是一项重要的优化措施，可以避免不必要的 re-layout。</p>
<p>当某个 Render Object 是 Relayout Boundary 时，会切断 layout dirty 向父节点传播，即下一帧刷新时父节点无需 re-layout。
<a href="https://zxfcumtcs.github.io/img/RelayoutBoundary.png">
        <a data-fancybox="gallery" href="https://luckly007.oss-cn-beijing.aliyuncs.com/image/RelayoutBoundary.png">
            <img class="mx-auto" alt="img" src="https://luckly007.oss-cn-beijing.aliyuncs.com/image/RelayoutBoundary.png" />
        </a>
    </a>
如上图：</p>
<ul>
<li>若<code>RD</code>节点出现 layout dirty，由于其自身、其父节点<code>RA</code>、<code>RRoot</code>都不是 Relayout Boundary，最终 layout dirty 传播到根节点<code>RenderView</code>，导致整颗『 RenderObject Tree 』重新布局；</li>
<li>若<code>RF</code>节点出现 layout dirty，由于其父节点<code>RB</code>为 Relayout Boundary，layout dirty 传播到<code>RB</code>即结束，最终需要重新布局的只有<code>RB</code>、<code>RF</code>两个节点；</li>
<li>若<code>RG</code>节点出现 layout dirty，由于其自身就是 Relayout Boundary，最终需要重新布局的只有<code>RG</code>自己。</li>
</ul>
<p>那么，具体来说要成为 Relayout Boundary 需要满足什么条件呢？</p>
<pre><code>void layout(Constraints constraints, { bool parentUsesSize = false }) {
  RenderObject relayoutBoundary;
  if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    relayoutBoundary = this;
  } 
  else {
    relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
  }

  if (!_needsLayout &amp;&amp; constraints == _constraints &amp;&amp; relayoutBoundary == _relayoutBoundary) {
    return;
  }

  if (_relayoutBoundary != null &amp;&amp; relayoutBoundary != _relayoutBoundary) {
    visitChildren(_cleanChildRelayoutBoundary);
  }
  _relayoutBoundary = relayoutBoundary;
}
</code></pre><p>以上是<code>RenderObject#layout</code>中与 Relayout Boundary 有关的代码，
可知满足以下 4 个条件之一即可成为 Relayout Boundary：</p>
<ul>
<li><code>parentUsesSize</code>为<code>false</code>，即父节点在 layout 时不会使用当前节点的 size 信息(也就是当前节点的排版信息对父节点无影响)；</li>
<li><code>sizedByParent</code>为<code>true</code>，即当前节点的 size 完全由父节点的 constraints 决定，即若在两次 layout 中传递下来的 constraints 相同，则两次 layout 后当前节点的 size 也相同；</li>
<li>传给当前节点的 constraints 是紧凑型 (Tight)，其效果与<code>sizedByParent</code>为<code>true</code>是一样的，即当前节点的 layout 不会改变其 size，size 由 constraints 唯一确定；</li>
<li>父节点不是 RenderObject 类型(主要针对根节点，其父节点为nil)。</li>
</ul>
<blockquote>
<p>每个 Render Object 都有一个<code>relayoutBoundary</code>属性，其值要么等于自己，要么等于父节点的<code>relayoutBoundary</code>。</p>
</blockquote>
<h2 id="markneedslayout">markNeedsLayout</h2>
<pre><code>void markNeedsLayout() {
  if (_needsLayout) {
    return;
  }
  if (_relayoutBoundary != this) {
    markParentNeedsLayout();
  } 
  else {
    _needsLayout = true;
    if (owner != null) {
      owner._nodesNeedingLayout.add(this);
      owner.requestVisualUpdate();
    }
  }
}
</code></pre><p>从上述代码可以看到：</p>
<ul>
<li>若当前 Render Object 不是 Relayout Boundary，则 layout 请求向上传播给父节点(即 layout 范围扩大到父节点，这是一个递归过程，直到遇到 Relayout Boundary)；</li>
<li>若当前 Render Object 是 Relayout Boundary，则 layout 请求到该节点为此，不会传播到其父节点。</li>
</ul>
<blockquote>
<p>通过 PipelineOwner 收集所有 layout dirty 节点，并在下一帧刷新时批量处理，而不是实时更新 dirty layout，从而避免不必要的重复 re-layout。</p>
</blockquote>
<h2 id="layout">layout</h2>
<pre><code>void layout(Constraints constraints, { bool parentUsesSize = false }) {
  RenderObject relayoutBoundary;
  if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    relayoutBoundary = this;
  } 
  else {
    relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
  }

  if (!_needsLayout &amp;&amp; constraints == _constraints &amp;&amp; relayoutBoundary == _relayoutBoundary) {
    return;
  }
  _constraints = constraints;
  if (_relayoutBoundary != null &amp;&amp; relayoutBoundary != _relayoutBoundary) {
    visitChildren(_cleanChildRelayoutBoundary);
  }
  _relayoutBoundary = relayoutBoundary;

  if (sizedByParent) {
    performResize();
  }

  performLayout();
  markNeedsSemanticsUpdate();
  
  _needsLayout = false;
  markNeedsPaint();
}
</code></pre><p><code>layout</code>方法是触发 Render Object 更新布局信息的主要入口点。
一般情况下，由父节点调用子节点的<code>layout</code>方法来更新其整体布局。
<code>RenderObject</code>的子类不应重写该方法，可按需重写<code>performResize</code>或/和<code>performLayout</code>方法。</p>
<p>当前 Render Object 的布局受到<code>layout</code>方法参数<code>constraints</code>的约束。
<a href="https://zxfcumtcs.github.io/img/LayputDataFlow.png">
        <a data-fancybox="gallery" href="https://luckly007.oss-cn-beijing.aliyuncs.com/image/LayputDataFlow.png">
            <img class="mx-auto" alt="img" src="https://luckly007.oss-cn-beijing.aliyuncs.com/image/LayputDataFlow.png" />
        </a>
    </a>
如上图，『 Render Object Tree 』的 layout 是一次深度优先遍历的过程。
优先 layout 子节点，之后 layout 父节点。
父节点向子节点传递 layout constraints，子节点在 layout 时需遵守这些约束。
作为子节点 layout 的结果，父节点在 layout 时可以使用子节点的 size。</p>
<p>在上述<code>layout</code>代码第<code>19~21</code>行，若<code>sizedByParent</code>为<code>true</code>，则调用<code>performResize</code>来计算该 Render Object 的 size。</p>
<p><code>sizedByParent</code>为<code>true</code>的 Render Object 需重写<code>performResize</code>方法，在该方法中仅根据<code>constraints</code>来计算 size。
如<code>RenderBox</code>中定义的<code>performResize</code>的默认行为：取<code>constraints</code>约束下的最小 size：</p>
<pre><code>@override
void performResize() {
  // default behavior for subclasses that have sizedByParent = true
  size = constraints.smallest;
  assert(size.isFinite);
}
</code></pre><p>若父节点 layout 依赖子节点的 size，在调用<code>layout</code>方法时需将<code>parentUsesSize</code>参数设为<code>true</code>。
因为，在这种情况下若子节点 re-layout 导致其 size 发生变化，需要及时通知父节点，父节点也需要 re-layout (即 layout dirty 范围需要向上传播)。
这一切都是通过上节介绍过的 Relayout Boundary 来实现。</p>
<h2 id="performlayout">performLayout</h2>
<p>本质上，<code>layout</code>是一个模板方法，具体的布局工作由<code>performLayout</code>方法完成。
<code>RenderObject#performLayout</code>是一个抽象方法，子类需重写。</p>
<p>关于<code>performLayout</code>有几点需要注意：</p>
<ul>
<li>该方法由<code>layout</code>方法调用，在需要 re-layout 时应调用<code>layout</code>方法，而不是<code>performLayout</code>；</li>
<li>若<code>sizedByParent</code>为<code>true</code>，则该方法不应改变当前 Render Object 的 size ( 其 size 由<code>performResize</code>方法计算)；</li>
<li>若<code>sizedByParent</code>为<code>false</code>，则该方法不仅要执行 layout 操作，还要计算当前 Render Object 的 size；</li>
<li>在该方法中，需对其所有子节点调用<code>layout</code>方法以执行所有子节点的 layout 操作，如果当前 Render Object 依赖子节点的布局信息，需将<code>parentUsesSize</code>参数设为<code>true</code>。</li>
</ul>
<pre><code>// RenderFlex
void performLayout() {
  RenderBox child = firstChild;
  while (child != null) {
    final FlexParentData childParentData = child.parentData;
    BoxConstraints innerConstraints = BoxConstraints(minHeight: constraints.maxHeight, maxHeight: constraints.maxHeight);
    child.layout(innerConstraints, parentUsesSize: true);
    child = childParentData.nextSibling;
  }
  
  size = constraints.constrain(Size(idealSize, crossSize));
  
  child = firstChild;
  while (child != null) {
    final FlexParentData childParentData = child.parentData;
    double childCrossPosition = crossSize / 2.0 - _getCrossSize(child) / 2.0;
    childParentData.offset = Offset(childMainPosition, childCrossPosition);
    child = childParentData.nextSibling;
  }
}
</code></pre><p>上述代码片段截取自<code>RenderFlex</code>，可以看到它大概做了3件事：</p>
<ul>
<li>对所有子节点逐个调用<code>layout</code>方法；</li>
<li>计算当前 Render Object 的 size；</li>
<li>将与子节点布局有关的信息存储到相应子节点的<code>parentData</code>中。</li>
</ul>
<blockquote>
<p><code>RenderFlex</code>继承自<code>RenderBox</code>，是常用的<code>Row</code>、<code>Column</code>对应的 Render Object。</p>
</blockquote>
<h1 id="绘制">绘制</h1>
<hr>
<p>与<code>markNeedsLayout</code>相似，当 Render Object 需要重新绘制 (paint dirty) 时通过<code>markNeedsPaint</code>方法上报给<code>PipelineOwner</code>。</p>
<h2 id="markneedspaint">markNeedsPaint</h2>
<pre><code>void markNeedsPaint() {
  if (isRepaintBoundary) {
    assert(_layer is OffsetLayer);
    if (owner != null) {
      owner._nodesNeedingPaint.add(this);
      owner.requestVisualUpdate();
    }
  } 
  else if (parent is RenderObject) {
    final RenderObject parent = this.parent;
    parent.markNeedsPaint();
  } 
  else {
    if (owner != null)
      owner.requestVisualUpdate();
  }
}
</code></pre><p><code>markNeedsPaint</code>内部逻辑与<code>markNeedsLayout</code>都非常相似：</p>
<ul>
<li>若当前 Render Object 是 Repaint Boundary，则将其添加到<code>PipelineOwner#_nodesNeedingPaint</code>中，Paint request 也随之结束；</li>
<li>否则，Paint request 向父节点传播，即需要 re-paint 的范围扩大到父节点(这是一个递归的过程)；</li>
<li>有一个特例，那就是『 Render Object Tree 』的根节点，即 RenderView，它的父节点为 nil，此时只需调用<code>PipelineOwner#requestVisualUpdate</code>即可。</li>
</ul>
<blockquote>
<p><code>PipelineOwner#_nodesNeedingPaint</code>收集的所有 Render Object 都是 Repaint Boundary。</p>
</blockquote>
<h2 id="repaint-boundary">Repaint Boundary</h2>
<p>对 Repaint Boundary 从上面<code>markNeedsPaint</code>的实现可略知一二，
若某 Render Object 是 Repaint Boundary，其会切断 re-Paint request 向父节点传播。</p>
<p>更直白点，Repaint Boundary 使得 Render Object 可以独立于父节点进行绘制，
否则当前 Render Object 会与父节点绘制在同一个 layer 上。
总结一下，Repaint Boundary 有以下特点：</p>
<ul>
<li>每个 Repaint Boundary 都有一个独属于自己的 OffsetLayer (ContainerLayer)，其自身及子孙节点的绘制结果都将 attach 到以该 layer 为根节点的子树上；</li>
<li>每个 Repaint Boundary 都有一个独属于自己的 PaintingContext (包括背后的 Canvas)，从而使得其绘制与父节点完全隔离开。</li>
</ul>
<p><a href="https://zxfcumtcs.github.io/img/RepaintBoundary.png">
        <a data-fancybox="gallery" href="https://zxfcumtcs.github.io/img/RepaintBoundary.png">
            <img class="mx-auto" alt="img" src="https://zxfcumtcs.github.io/img/RepaintBoundary.png" />
        </a>
    </a>
如上图，由于<code>Root</code>/<code>RA</code>/<code>RC</code>/<code>RG</code>/<code>RI</code>是 Repaint Boundary，所以它们都有对应的 OffsetLayer。
同时，由于每个 Repaint Boundary 都有属于自己的 PaintingContext，所以它们都有对应的 PictureLayer，用于呈现具体的绘制结果。
对于那些不是 Repaint Boundary 的节点，将会绘制到最近的 Repaint Boundary 祖先节点提供的 PictureLayer 上。</p>
<blockquote>
<p>Repaint Boundary 会影响兄弟节点的绘制，如由于<code>RC</code>是 Repaint Boundary，导致<code>RB</code>、<code>RD</code>被绘制到不同的 PictureLayer 上。</p>
</blockquote>
<blockquote>
<p>实现中，『 Layer Tree 』往往会比上图所示更复杂，由于每个 Render Object 在绘制过程中都可以自主引入更多的 layer。</p>
</blockquote>
<p>Repaint Boundary 的目标是优化性能，但从上面的讨论我们也可以看出 Repaint Boundary 会增加『 Layer Tree 』的复杂度。
因此，Repaint Boundary 并不是越多越好。
只适用于那些需要频繁重绘的场景，如视频。</p>
<blockquote>
<p>Flutter Framework 为开发者预定义了<code>RepaintBoundary</code> widget，其继承自<code>SingleChildRenderObjectWidget</code>，在有需要时我们可以通过<code>RepaintBoundary</code> widget 来添加 Repaint Boundary。</p>
</blockquote>
<h2 id="paint">Paint</h2>
<pre><code>void paint(PaintingContext context, Offset offset) { }
</code></pre><p>抽象基类<code>RenderObject</code>中的<code>paint</code>是个空方法，需要子类重写。
<code>paint</code>方法主要有2项任务：</p>
<ul>
<li>
<p>当前 Render Object 本身的绘制，如：<code>RenderImage</code>，其<code>paint</code>方法主要职责就是 image 的渲染</p>
<pre><code>void paint(PaintingContext context, Offset offset) {
  paintImage(
    canvas: context.canvas,
    rect: offset &amp; size,
    image: _image,
    ...
  );
}
</code></pre></li>
<li>
<p>绘制子节点，如：<code>RenderTable</code>，其<code>paint</code>方法主要职责是依次对每个子节点调用<code>PaintingContext#paintChild</code>方法进行绘制：</p>
<pre><code>void paint(PaintingContext context, Offset offset) {
  for (int index = 0; index &lt; _children.length; index += 1) {
    final RenderBox child = _children[index];
    if (child != null) {
      final BoxParentData childParentData = child.parentData;
      context.paintChild(child, childParentData.offset + offset);
    }
  }
}
</code></pre></li>
</ul>
<h2 id="串起来">串起来</h2>
<p><a href="https://zxfcumtcs.github.io/img/RenderObject-Paint-Line.png">
        <a data-fancybox="gallery" href="https://zxfcumtcs.github.io/img/RenderObject-Paint-Line.png">
            <img class="mx-auto" alt="img" src="https://zxfcumtcs.github.io/img/RenderObject-Paint-Line.png" />
        </a>
    </a>
下面我们将整个绘制流程串一串，如上图：</p>
<h3 id="pipelineownerflushpaint">PipelineOwner#flushPaint</h3>
<p>当新一帧开始时，会触发<code>PipelineOwner#flushPaint</code>方法，进而对收集的所有『 paint-dirty Render Obejcts 』进行 re-paint：</p>
<pre><code>void flushPaint() {
    try {
      final List&lt;RenderObject&gt; dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = &lt;RenderObject&gt;[];
      
      // Sort the dirty nodes in reverse order (deepest first).
      for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) =&gt; b.depth - a.depth)) {
        assert(node._layer != null);
        if (node._needsPaint &amp;&amp; node.owner == this) {
          PaintingContext.repaintCompositedChild(node);
        }
      }
    } 
  }
</code></pre><h3 id="paintingcontextrepaintcompositedchild">PaintingContext#repaintCompositedChild</h3>
<p><code>PaintingContext#repaintCompositedChild</code>是一个非常重要的方法，一是为『 paint-dirty Render Obejcts 』创建 Layer (若没有)、二是为 RenderObject 的绘制准备<code>context</code>并发起绘制流程：</p>
<pre><code>static void _repaintCompositedChild(
  RenderObject child, {
  bool debugAlsoPaintedParent = false,
  PaintingContext childContext,
}) {
  assert(child.isRepaintBoundary);
  OffsetLayer childLayer = child._layer;
  if (childLayer == null) {
    child._layer = childLayer = OffsetLayer();
  } 
  else {
    assert(childLayer is OffsetLayer);
    childLayer.removeAllChildren();
  }
  
  // 在正常的绘制流程中通过参数传递过来的 childContext 都是 null
  // 因此，此处总是会创建新的 PaintingContext
  //
  childContext ??= PaintingContext(child._layer, child.paintBounds);
  child._paintWithContext(childContext, Offset.zero);

  childContext.stopRecordingIfNeeded();
}
</code></pre><h3 id="renderobject_paintwithcontext">RenderObject#_paintWithContext</h3>
<p><code>RenderObject#_paintWithContext</code>逻辑比较简单，主要就是调用<code>paint</code>方法；</p>
<pre><code>void _paintWithContext(PaintingContext context, Offset offset) {
  if (_needsLayout)
    return;
  _needsPaint = false;
  paint(context, offset);
}
</code></pre><p><code>paint</code>方法正如上节所讲，通过<code>Canvas#draw***</code>系列方法进行具体的绘制操作，对于子节点(若有)调用<code>PaintingContext#paintChild</code>方法；</p>
<h3 id="paintingcontextpaintchild">PaintingContext#paintChild</h3>
<p>对于当前绘制子节点，若是 Repaint Boundary，则需要在独立的 layer 上进行绘制，
否则直接调用子节点的<code>_paintWithContext</code>方法在当前上下文(paint context)中绘制：</p>
<pre><code>void paintChild(RenderObject child, Offset offset) {
  if (child.isRepaintBoundary) {
    stopRecordingIfNeeded();
    _compositeChild(child, offset);
  } 
  else {
    child._paintWithContext(this, offset);
  }
}
</code></pre><p>下面重点分析对 Repaint Boundary 的处理，如上<code>paintChild</code>所示，首先会调用<code>PaintingContext#stopRecordingIfNeeded</code>来停止当前的绘制工作：</p>
<pre><code>void stopRecordingIfNeeded() {
  if (!_isRecording)
    return;
  _currentLayer.picture = _recorder.endRecording();
  _currentLayer = null;
  _recorder = null;
  _canvas = null;
}
</code></pre><p><code>stopRecordingIfNeeded</code>首先将当前绘制结果保存到<code>_currentLayer.picture</code>中，之后对上下文做一些清理。
将<code>_currentLayer</code>置为 null，初看感觉难于理解，保存在<code>_currentLayer.picture</code>中的绘制结果且不是就丢了？
其实，<code>_currentLayer</code>早在<code>_startRecording</code>方法中就被添加到『 Layer Tree 』上，这里只是将<code>PaintingContext</code>中的引用置为 null 而以。</p>
<pre><code>void _startRecording() {
  assert(!_isRecording);
  _currentLayer = PictureLayer(estimatedBounds);
  _recorder = ui.PictureRecorder();
  _canvas = Canvas(_recorder);
  _containerLayer.append(_currentLayer);
}
</code></pre><p>由于已经将<code>_canvas</code>置为 null 了，下次使用时会触发对<code>_startRecording</code>方法的调用：</p>
<pre><code>Canvas get canvas {
  if (_canvas == null)
    _startRecording();
  return _canvas;
}
</code></pre><h3 id="paintingcontext_compositechild">PaintingContext#_compositeChild</h3>
<p>在<code>_compositeChild</code>中，通过<code>repaintCompositedChild</code>对子节点发起新一轮的绘制，并将绘制结果(<code>child._layer</code>)添加到『 Layer Tree 』中：</p>
<pre><code>void _compositeChild(RenderObject child, Offset offset) {
  assert(!_isRecording);
  assert(child.isRepaintBoundary);
  assert(_canvas == null || _canvas.getSaveCount() == 1);

  repaintCompositedChild(child, debugAlsoPaintedParent: true);

  final OffsetLayer childOffsetLayer = child._layer;
  childOffsetLayer.offset = offset;
  appendLayer(child._layer);
}
</code></pre><h3 id="小结">小结</h3>
<p>整个绘制过程其实就是对『 RenderObject Tree 』进行深度遍历的过程；
Repaint Boundary 会独立于父节点进行绘制，因此需要独立的 ContainerLayer(OffsetLayer) 以及 PaintingContext。</p>
<h1 id="跑起来">跑起来</h1>
<hr>
<p>下面我们简单的分析一下，一个 Flutter App 是怎么跑起来的。</p>
<pre><code>void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}
</code></pre><p><code>runApp</code>是 Flutter 项目的入口点，完成 Binding 的初始化、attach root widget、安排首帧的调度。</p>
<p><a href="https://zxfcumtcs.github.io/img/RenderView-create.png">
        <a data-fancybox="gallery" href="https://zxfcumtcs.github.io/img/RenderView-create.png">
            <img class="mx-auto" alt="img" src="https://zxfcumtcs.github.io/img/RenderView-create.png" />
        </a>
    </a>
如上图，在<code>RendererBinding#initInstances</code>中会创建『 RenderObject Tree 』的根节点：<code>RenderView</code>。</p>
<p>如下代码，在<code>RenderView</code>初始化过程中为首帧渲染作准备，『 Layer Tree 』根节点也是在此过程中创建的。</p>
<pre><code>// RenderView
//
void prepareInitialFrame() {
  scheduleInitialLayout();
  scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
}

Layer _updateMatricesAndCreateNewRootLayer() {
  _rootTransform = configuration.toMatrix();
  final ContainerLayer rootLayer = TransformLayer(transform: _rootTransform);
  rootLayer.attach(this);
  return rootLayer;
}
</code></pre><pre><code>// RenderObject
//
void scheduleInitialLayout() {
  _relayoutBoundary = this;
  owner._nodesNeedingLayout.add(this);
}

void scheduleInitialPaint(ContainerLayer rootLayer) {
  _layer = rootLayer;
  owner._nodesNeedingPaint.add(this);
}
</code></pre><p>如下图，在<code>WidgetsFlutterBinding#scheduleAttachRootWidget</code>-&gt;<code>WidgetsFlutterBinding#attachRootWidget</code>调用链上会创建 Root Widget：<code>RenderObjectToWidgetAdapter</code>。</p>
<p><a href="https://zxfcumtcs.github.io/img/attachRootWidget.png">
        <a data-fancybox="gallery" href="https://zxfcumtcs.github.io/img/attachRootWidget.png">
            <img class="mx-auto" alt="img" src="https://zxfcumtcs.github.io/img/attachRootWidget.png" />
        </a>
    </a></p>
<p>在<code>RenderObjectToWidgetAdapter#attachToRenderTree</code>方法中『 Element Tree 』的根节点<code>RenderObjectToWidgetElement</code>被创建出来。
至此：</p>
<ul>
<li>Root Widget(<code>RenderObjectToWidgetAdapter</code>)</li>
<li>『 Element Tree 』的根节点(<code>RenderObjectToWidgetElement</code>)</li>
<li>『 RenderObject Tree 』的根节点(<code>RenderView</code>)</li>
<li>『 Layer Tree 』的根节点(<code>TransformLayer</code>)</li>
</ul>
<p>创建完成。</p>
<pre><code>// RenderObjectToWidgetAdapter
RenderObjectToWidgetElement&lt;T&gt; attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement&lt;T&gt; element ]) {
 owner.lockState(() {
   element = createElement();
   element.assignOwner(owner);
 });
 
 owner.buildScope(element, () {
   element.mount(null, null);
 });
 
 // This is most likely the first time the framework is ready to produce
 // a frame. Ensure that we are asked for one.
 SchedulerBinding.instance.ensureVisualUpdate();

  return element;
}

RenderObjectToWidgetElement&lt;T&gt; createElement() =&gt; RenderObjectToWidgetElement&lt;T&gt;(this);
</code></pre><p>上述代码第<code>9</code>行，对 Root Element 进行挂载(mount)。
随之，『 Element Tree 』被逐步创建出来。</p>
<blockquote>
<p>关于『 Element Tree 』创建过程更详细的分析可参看<a href="https://zxfcumtcs.github.io/2020/05/17/deepinto-flutter-element/">『深入浅出 Flutter Framework 之 Element』</a>一文</p>
</blockquote>
<p><a href="https://zxfcumtcs.github.io/img/RenderObjectToWidgetElement-mount.png">
        <a data-fancybox="gallery" href="https://zxfcumtcs.github.io/img/RenderObjectToWidgetElement-mount.png">
            <img class="mx-auto" alt="img" src="https://zxfcumtcs.github.io/img/RenderObjectToWidgetElement-mount.png" />
        </a>
    </a></p>
<pre><code>// RenderObjectToWidgetAdapter
void _rebuild() {
  _child = updateChild(_child, widget.child, _rootChildSlot);
}
</code></pre><p>随着『 Element Tree 』的构建，『 RenderObject Tree 』也被创建出来。</p>
<pre><code>// RenderObjectElement
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}
</code></pre><h1 id="小结-1">小结</h1>
<hr>
<p>本文对 RenderObject 生命周期中几个重要节点：创建、布局、绘制等作了简要介绍。
对一些重要概念，如：Relayout Boundary、Repaint Boundary 也进行了较详细的分析。</p>

        </div>

        
<div class="post-archive">
    <ul class="post-copyright">
        <li><strong>原文作者：</strong><a rel="author" href="https://luckly.work/">luckly</a></li>
        <li style="word-break:break-all"><strong>原文链接：</strong><a href="https://luckly.work/post/flutter_Framework/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E4%B9%8BRenderObject/">https://luckly.work/post/flutter_Framework/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E4%B9%8BRenderObject/</a></li>
        <li><strong>版权声明：</strong>本作品采用<a rel="license" href="https://creativecommons.org/licenses/by-nc-nd/4.0/">知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议</a>进行许可，非商业转载请注明出处（作者，原文链接），商业转载请联系作者获得授权。</li>
    </ul>
</div>
<br/>



        

<div class="post-archive">
    <h2>See Also</h2>
    <ul class="listing">
        
        <li><a href="/post/flutter_Framework/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E4%B9%8BPipelineOwner/">深入浅出之PipelineOwner</a></li>
        
        <li><a href="/post/flutter_Framework/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E4%B9%8BlAYER/">深入浅出之Layer</a></li>
        
        <li><a href="/post/flutter_Framework/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E4%B9%8BPaintingContext/">深入浅出之PaintingContext</a></li>
        
        <li><a href="/post/flutter_Framework/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E4%B9%8BElement/">深入浅出之Element</a></li>
        
        <li><a href="/post/flutter_Framework/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BA%E4%B9%8BBuildOwner/">深入浅出之BuildOwner</a></li>
        
    </ul>
</div>


        <div class="post-meta meta-tags">
            
            <ul class="clearfix">
                
                <li><a href='/tags/Flutter'>Flutter</a></li>
                
            </ul>
            
        </div>
    </article>
    
    

    
    
    <div class="post bg-white">
      <script src="https://utteranc.es/client.js"
            repo= "https://github.com/ITmxs/repo"
            issue-term="pathname"
            theme="github-light"
            crossorigin="anonymous"
            async>
      </script>
    </div>
    
</div>

                    <footer id="footer">
    <div>
        &copy; 2021 <a href="https://luckly.work/">早起的年轻人 By luckly</a>
        
        | <a rel="nofollow" target="_blank" href="http://beian.miit.gov.cn/">粤ICP备2021号-1</a>
        
    </div>
    <br />
    <div>
        <div class="github-badge">
            <a href="https://juejin.cn/user/3843548384077192" target="_black" rel="nofollow"><span class="badge-subject">Powered by</span><span class="badge-value bg-blue">掘金</span></a>
        </div>
        <div class="github-badge">
            <a href="https://space.bilibili.com/480883651" target="_black"><span class="badge-subject">Design by</span><span class="badge-value bg-brightgreen">早起的年轻人</span></a>
        </div>
        <div class="github-badge">
            <a href="https://cloud.tencent.com/developer/user/6702670" target="_black"><span class="badge-subject">Theme</span><span class="badge-value bg-yellowgreen">云社区</span></a>
        </div>
    </div>
</footer>


    
    <script type="text/javascript">
        window.MathJax = {
            tex2jax: {
                inlineMath: [['$', '$']],
                processEscapes: true
                }
            };
    </script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML' async></script><script src="https://cdn.bootcdn.net/ajax/libs/fancybox/3.5.7/jquery.fancybox.min.js"></script>

<a id="rocket" href="#top"></a>
<script type="text/javascript" src='/js/totop.js?v=0.0.0' async=""></script>



    <script type="text/javascript" src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" async></script>




    <script src='/js/douban.js'></script>
    <script src="/js/copy-to-clipboard.js"></script>

                </div>

                <div id="secondary">
    <section class="widget">
        <form id="search" action='https://luckly.work/search/' method="get" accept-charset="utf-8" target="_blank" _lpchecked="1">
      
      <input type="text" name="q" maxlength="20" placeholder="Search">
      <input type="hidden" name="sitesearch" value="https://luckly.work/">
      <button type="submit" class="submit icon-search"></button>
</form>
    </section>
    
    <section class="widget">
        <h3 class="widget-title">最近文章</h3>
<ul class="widget-list">
    
    <li>
        <a href="https://luckly.work/post/nginx/nginx%E6%96%87%E4%BB%B6%E5%86%85%E5%AE%B9/" title="Nginx文件内容">Nginx文件内容</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/nginx/nginx%E6%9E%81%E7%AE%80%E6%95%99%E7%A8%8B/" title="Nginx极简教程">Nginx极简教程</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/nginx/%E5%8D%81%E5%88%86%E9%92%9F%E5%85%A5%E9%97%A8nginx/" title="十分钟入门Nginx">十分钟入门Nginx</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/go/Goland%E8%BF%9C%E7%A8%8B%E5%BC%80%E5%8F%91%E9%85%8D%E7%BD%AE/" title="Goland远程开发配置">Goland远程开发配置</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/%E8%AF%BB%E4%B9%A6/%E5%9F%9F%E5%90%8D%E8%A7%A3%E6%9E%90/" title="域名解析">域名解析</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/git/%E6%8F%90%E4%BA%A4%E8%BF%87%E7%A8%8B%E7%9C%81%E7%95%A5%E6%9F%90%E4%BA%9B%E6%96%87%E4%BB%B6/" title="提交过程省略某些文件">提交过程省略某些文件</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/flutter_tips/Flutter_DropdownButton%E7%A4%BA%E4%BE%8B/" title="Flutter_DropdownButton示例">Flutter_DropdownButton示例</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/flutter_tips/Flutter_ExpansionPanelList%E5%92%8CExpansionPanelList.radio%E7%A4%BA%E4%BE%8B/" title="Flutter_ExpansionPanelList和ExpansionPanelList">Flutter_ExpansionPanelList和ExpansionPanelList</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/flutter_tips/Flutter%E5%BE%AE%E4%BF%A1%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E8%B7%B3%E5%9B%9EApp%E6%8C%87%E5%AE%9A%E9%A1%B5%E9%9D%A2/" title="Flutter微信分享链接跳回App指定页面">Flutter微信分享链接跳回App指定页面</a>
    </li>
    
    <li>
        <a href="https://luckly.work/post/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F/%E5%A4%96%E5%8C%85%E5%8F%AF%E8%83%BD%E7%9A%84%E9%97%AE%E9%A2%98/" title="外包可能的问题">外包可能的问题</a>
    </li>
    
</ul>
    </section>

    

    <section class="widget">
        <h3 class="widget-title"><a href='/categories/'>分类</a></h3>
<ul class="widget-list">
    
    <li><a href="https://luckly.work/categories/Flutter/">Flutter (326)</a></li>
    
    <li><a href="https://luckly.work/categories/IT/">IT (2)</a></li>
    
    <li><a href="https://luckly.work/categories/Kotlin/">Kotlin (2)</a></li>
    
    <li><a href="https://luckly.work/categories/Mysql/">Mysql (1)</a></li>
    
    <li><a href="https://luckly.work/categories/nginx/">nginx (1)</a></li>
    
    <li><a href="https://luckly.work/categories/Vue/">Vue (6)</a></li>
    
    <li><a href="https://luckly.work/categories/YouTube%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD/">YouTube视频下载 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/android/">android (6)</a></li>
    
    <li><a href="https://luckly.work/categories/dart/">dart (96)</a></li>
    
    <li><a href="https://luckly.work/categories/Flutter/">Flutter (28)</a></li>
    
    <li><a href="https://luckly.work/categories/gin/">gin (25)</a></li>
    
    <li><a href="https://luckly.work/categories/git/">git (4)</a></li>
    
    <li><a href="https://luckly.work/categories/Go/">Go (102)</a></li>
    
    <li><a href="https://luckly.work/categories/gorm/">gorm (4)</a></li>
    
    <li><a href="https://luckly.work/categories/grpc/">grpc (1)</a></li>
    
    <li><a href="https://luckly.work/categories/html/">html (3)</a></li>
    
    <li><a href="https://luckly.work/categories/ios/">ios (1)</a></li>
    
    <li><a href="https://luckly.work/categories/linux/">linux (1)</a></li>
    
    <li><a href="https://luckly.work/categories/nginx/">nginx (6)</a></li>
    
    <li><a href="https://luckly.work/categories/python/">python (35)</a></li>
    
    <li><a href="https://luckly.work/categories/read/">读书笔记 (6)</a></li>
    
    <li><a href="https://luckly.work/categories/redis/">redis (2)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%B8%AA%E4%BA%BA%E8%B5%84%E6%96%99/">个人资料 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%B9%A6%E5%8D%95/">书单 (8)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%B9%A6%E8%AF%84/">书评 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E7%AE%A1%E7%90%86%E5%B8%88/">信息系统管理师 (19)</a></li>
    
    <li><a href="https://luckly.work/categories/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86%E5%B8%88/">信息系统项目管理师 (25)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%8E%9F%E5%88%99/">原则 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%8E%9F%E7%94%9F%E9%80%9A%E8%AE%AF/">原生通讯 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%9F%BA%E7%A1%80/">基础 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%A4%8D%E5%88%A9%E6%95%88%E5%BA%94/">复利效应 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B/">安装教程 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%B0%91%E6%9C%89%E4%BA%BA%E8%B5%B0%E7%9A%84%E8%B7%AF/">少有人走的路 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E5%BF%83%E8%AF%AD/">心语 (3)</a></li>
    
    <li><a href="https://luckly.work/categories/%E6%8F%92%E4%BB%B6/">插件 (2)</a></li>
    
    <li><a href="https://luckly.work/categories/%E6%95%99%E5%AD%A6/">教学 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%8E%8B%E9%98%B3%E6%98%8E/">王阳明 (3)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%94%B5%E5%AD%90%E4%B9%A6/">电子书 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%99%BB%E9%99%86%E8%A1%A8%E5%8D%95/">登陆表单 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%A8%BB%E7%9B%9B%E5%92%8C%E5%A4%AB/">稻盛和夫 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%A9%B7%E7%88%B8%E7%88%B8%E5%AF%8C%E7%88%B8%E7%88%B8/">穷爸爸富爸爸 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%B2%BE%E8%BF%9B/">精进 (3)</a></li>
    
    <li><a href="https://luckly.work/categories/%E7%BC%96%E7%A8%8B/">编程 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%99%9A%E5%B9%BB/">虚幻 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%B4%A2%E5%8A%A1%E8%87%AA%E7%94%B1%E4%B9%8B%E8%B7%AF/">财务自由之路 (2)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%B7%91%E6%AD%A5/">跑步 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%B7%AF%E7%94%B1%E4%BC%A0%E5%8F%82/">路由传参 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%B7%AF%E7%BA%BF/">路线 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E8%BD%AF%E4%BB%B6%E5%AE%9E%E6%96%BD/">软件实施 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E9%98%B3%E6%98%8E%E5%BF%83%E5%AD%A6/">阳明心学 (3)</a></li>
    
    <li><a href="https://luckly.work/categories/%E9%A1%B9%E7%9B%AE/">项目 (1)</a></li>
    
    <li><a href="https://luckly.work/categories/%E9%AD%85%E5%8A%9B/">魅力 (1)</a></li>
    
</ul>
    </section>

    <section class="widget">
        <h3 class="widget-title"><a href='/tags/'>标签</a></h3>
<div class="tagcloud">
    
    <a href="https://luckly.work/tags/flutter/">flutter</a>
    
    <a href="https://luckly.work/tags/IT/">IT</a>
    
    <a href="https://luckly.work/tags/Kotlin/">Kotlin</a>
    
    <a href="https://luckly.work/tags/Mysql/">Mysql</a>
    
    <a href="https://luckly.work/tags/nginx/">nginx</a>
    
    <a href="https://luckly.work/tags/Vue/">Vue</a>
    
    <a href="https://luckly.work/tags/YouTube%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD/">YouTube视频下载</a>
    
    <a href="https://luckly.work/tags/android/">android</a>
    
    <a href="https://luckly.work/tags/dart/">dart</a>
    
    <a href="https://luckly.work/tags/flutter/">flutter</a>
    
    <a href="https://luckly.work/tags/gin/">gin</a>
    
    <a href="https://luckly.work/tags/git/">git</a>
    
    <a href="https://luckly.work/tags/go/">go</a>
    
    <a href="https://luckly.work/tags/gorm/">gorm</a>
    
    <a href="https://luckly.work/tags/grpc/">grpc</a>
    
    <a href="https://luckly.work/tags/html/">html</a>
    
    <a href="https://luckly.work/tags/ios/">ios</a>
    
    <a href="https://luckly.work/tags/linux/">linux</a>
    
    <a href="https://luckly.work/tags/nginx/">nginx</a>
    
    <a href="https://luckly.work/tags/python/">python</a>
    
    <a href="https://luckly.work/tags/redis/">redis</a>
    
    <a href="https://luckly.work/tags/%E4%B8%AA%E4%BA%BA%E8%B5%84%E6%96%99/">个人资料</a>
    
    <a href="https://luckly.work/tags/%E4%B9%A6%E5%8D%95/">书单</a>
    
    <a href="https://luckly.work/tags/%E4%B9%A6%E8%AF%84/">书评</a>
    
    <a href="https://luckly.work/tags/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E7%AE%A1%E7%90%86%E5%B8%88/">信息系统管理师</a>
    
    <a href="https://luckly.work/tags/%E4%BF%A1%E6%81%AF%E7%B3%BB%E7%BB%9F%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86%E5%B8%88/">信息系统项目管理师</a>
    
    <a href="https://luckly.work/tags/%E5%85%A5%E9%97%A8/">入门</a>
    
    <a href="https://luckly.work/tags/%E5%8E%9F%E5%88%99/">原则</a>
    
    <a href="https://luckly.work/tags/%E5%8E%9F%E7%94%9F%E9%80%9A%E8%AE%AF/">原生通讯</a>
    
    <a href="https://luckly.work/tags/%E5%9F%BA%E7%A1%80/">基础</a>
    
    <a href="https://luckly.work/tags/%E5%A4%8D%E5%88%A9%E6%95%88%E5%BA%94/">复利效应</a>
    
    <a href="https://luckly.work/tags/%E5%AE%89%E8%A3%85%E6%95%99%E7%A8%8B/">安装教程</a>
    
    <a href="https://luckly.work/tags/%E5%B0%91%E6%9C%89%E4%BA%BA%E8%B5%B0%E7%9A%84%E8%B7%AF/">少有人走的路</a>
    
    <a href="https://luckly.work/tags/%E5%BF%83%E8%AF%AD/">心语</a>
    
    <a href="https://luckly.work/tags/%E6%8F%92%E4%BB%B6/">插件</a>
    
    <a href="https://luckly.work/tags/%E6%95%99%E5%AD%A6/">教学</a>
    
    <a href="https://luckly.work/tags/%E7%8E%8B%E9%98%B3%E6%98%8E/">王阳明</a>
    
    <a href="https://luckly.work/tags/%E7%94%B5%E5%AD%90%E4%B9%A6/">电子书</a>
    
    <a href="https://luckly.work/tags/%E7%99%BB%E9%99%86%E8%A1%A8%E5%8D%95/">登陆表单</a>
    
    <a href="https://luckly.work/tags/%E7%A8%BB%E7%9B%9B%E5%92%8C%E5%A4%AB/">稻盛和夫</a>
    
    <a href="https://luckly.work/tags/%E7%A9%B7%E7%88%B8%E7%88%B8%E5%AF%8C%E7%88%B8%E7%88%B8/">穷爸爸富爸爸</a>
    
    <a href="https://luckly.work/tags/%E7%B2%BE%E8%BF%9B/">精进</a>
    
    <a href="https://luckly.work/tags/%E7%BC%96%E7%A8%8B/">编程</a>
    
    <a href="https://luckly.work/tags/%E8%99%9A%E5%B9%BB/">虚幻</a>
    
    <a href="https://luckly.work/tags/%E8%AF%97/">诗</a>
    
    <a href="https://luckly.work/tags/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/">读书笔记</a>
    
    <a href="https://luckly.work/tags/%E8%B4%A2%E5%8A%A1%E8%87%AA%E7%94%B1%E4%B9%8B%E8%B7%AF/">财务自由之路</a>
    
    <a href="https://luckly.work/tags/%E8%B7%91%E6%AD%A5/">跑步</a>
    
    <a href="https://luckly.work/tags/%E8%B7%AF%E7%94%B1%E4%BC%A0%E5%8F%82/">路由传参</a>
    
    <a href="https://luckly.work/tags/%E8%B7%AF%E7%BA%BF/">路线</a>
    
    <a href="https://luckly.work/tags/%E8%BD%AF%E4%BB%B6%E5%AE%9E%E6%96%BD/">软件实施</a>
    
    <a href="https://luckly.work/tags/%E9%80%9A%E8%AE%AF%E5%BD%95/">通讯录</a>
    
    <a href="https://luckly.work/tags/%E9%98%B3%E6%98%8E%E5%BF%83%E5%AD%A6/">阳明心学</a>
    
    <a href="https://luckly.work/tags/%E9%A1%B9%E7%9B%AE/">项目</a>
    
    <a href="https://luckly.work/tags/%E9%AD%85%E5%8A%9B/">魅力</a>
    
</div>
    </section>

    
<section class="widget">
    <h3 class="widget-title">友情链接</h3>
    <ul class="widget-list">
        
        <li>
            <a target="_blank" href="http://www.topgoer.com/" title="枯藤">枯藤</a>
        </li>
        
        <li>
            <a target="_blank" href="https://gorm.cn/zh_CN/docs/index.html" title="gorm">gorm</a>
        </li>
        
        <li>
            <a target="_blank" href="https://docs.python.org/zh-cn/3/tutorial/index.html" title="python">python</a>
        </li>
        
        <li>
            <a target="_blank" href="https://www.liwenzhou.com/" title="李文周">李文周的博客</a>
        </li>
        
        <li>
            <a target="_blank" href="http://www.xbzweb.com/" title="小包子的博客">小包子的博客</a>
        </li>
        
        <li>
            <a target="_blank" href="https://www.flysnow.org/" title="飞雪无情的博客">飞雪无情的博客</a>
        </li>
        
        <li>
            <a target="_blank" href="https://sliverhorn.com/" title="sliverhorn的博客">sliverhorn的博客</a>
        </li>
        
        <li>
            <a target="_blank" href="http://yuedu.baidu.com/ebook/14a722970740be1e640e9a3e" title="Android Gradle权威指南">Android Gradle权威指南</a>
        </li>
        
        <li>
            <a target="_blank" href="https://gesdh.cn/" title="小格子">格子导航</a>
        </li>
        
        <li>
            <a target="_blank" href="https://itachi.xyz/" title="阿林">itachi&#39;s Blog</a>
        </li>
        
        <li>
            <a target="_blank" href="https://darjun.github.io/" title="大俊">大俊Blog</a>
        </li>
        
        <li>
            <a target="_blank" href="https://geektutu.com/post/quick-golang.html" title="极客兔兔">极客兔兔Blog</a>
        </li>
        
        <li>
            <a target="_blank" href="http://zxfcumtcs.github.io/" title="赵雪峰">雪峰Blog</a>
        </li>
        
    </ul>
</section>


    <section class="widget">
        <h3 class="widget-title">其它</h3>
        <ul class="widget-list">
            <li><a href="https://luckly.work/index.xml">文章 RSS</a></li>
        </ul>
    </section>
</div>
            </div>
        </div>
    </div>
</body>

</html>